JVM Runtime Data Area总结

JVM Runtime Data Area

Runtime Data Area 是存放数据的。分为五部分:Stack、Heap、Method Area、PC Register、Native Method Stack。几乎所有的关于 Java 内存方面的问题,都是集中在这块。

运行时数据区介绍

Java 中的运⾏时数据可以划分为两部分,⼀部分是线程私有的,包括 虚拟机栈本地⽅法栈程序计数器,另⼀部分是线程共享的,包括 方法区。其中线程私有内存区会随线程产生和消亡,因此不需要过多考虑内存回收的问题,并且它在编译时就确定了所需内存的大小。
p4f0w

线程私有:随着线程消亡而自动回收,不需要 GC 管理

  1. 虚拟机栈 VM Stack:方法执行的内存区,每个方法执行时会在虚拟机栈中创建栈帧
  2. 程序计数器 PC Register:记录正在执行的虚拟机字节码地址
  3. 本地方法栈 Native Method Stack:虚拟机 native 方法执行的内存区

线程共享:GC 管理

  1. 堆 Heap:new 出来的对象内存区域
  2. 方法区 Method Area:存放类信息、常量、静态变量、编译器编译后的代码等数据;常量池:存放编译器生成的各种字面量和符号引用,是方法区的一部分

VM Stack 虚拟机栈

线程私有,FILO 数据结构。

虚拟机栈描述的是 Java 方法执行的内存模型,虚拟机栈存储着当前线程运行方法所需的数据,指令、返回地址。虚拟机栈里的每条数据,就是栈帧。
在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦完成相应的调用,则出栈。所有的栈帧都出栈后,线程也就结束了。
StackFrame包含主要包含信息:局部变量表、操作数栈、动态连接地址、返回地址。
JVM 的指令集是基于栈而不是寄存器,基于栈可以具备很好的跨平台性。
栈之 GC
栈是不需要垃圾回收的,栈中的对象如果用垃圾回收的观点来看,他永远是 live 状态,是可以 reachable 的,所以也不需要回收,它占有的空间随着 Thread 的结束而释放。
虚拟机栈执行示例
bgupk

PC Register 程序计数器

线程私有,不会发生 OOM

程序计数器是一块很小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器;主要用来记录各个线程执行的字节码的地址(例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器)
为什么需要程序计数器?
因为 Java 是多线程的,意味着线程切换。使用程序计数器能确保多线程情况下的程序正常执行。

Native Method Stack 本地方法栈

线程私有。给 native 方法使用的栈,每个线程持有一个 Native Method Stack。

和虚拟机栈所发挥的作⽤是⾮常相似的,只不过本地⽅法栈描述的是 Native ⽅法执⾏的内存模型。这⼀块虚拟机规范是⽆强制规定的,各版本虚拟机⾃由实现,⽽ HotSpot 则直接把本地⽅法栈和虚拟机栈合⼆为⼀了。

Method Area 方法区

方法区主要是用来存放已被虚拟机**加载的类相关信息,包括类信息、静态变量、常量、运行时常量池、字符串常量池。**JVM 对⽅法区的限制⽐较宽松,除了和 Java 堆⼀样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾回收。相对而言,垃圾回收在这个区域是比较少出现的。

方法区是一种规范,不同的虚拟机不同的版本实现不一样。

很多开发者习惯将方法区称为 永久代,其实两者并不是等价的,HotSpot 虚拟机只是使用了永久代来实现方法区,但是在 JDK8.0+ 已经将方法区中实现的永久代去掉了,用元空间替换,元空间的存储位置是本地内存。

JDK8.0 为什么使用元空间替换永久代?

运行时常量池在不同版本的位置

HEAP 堆

所有线程共享,伴随着 JVM 的启动而创建,负责存储所有对象实例和数组的;GC 管理的主要区域。

Heap 组成

堆的存储空间和栈一样是不需要连续的。
现代收集器基本上都是分代回收,Heap 还可以分为 Young GenerationOld Generation (也叫 Tenured Generation)两大部分。Young Generation 分为 EdenSurvivor,Survivor 又分为 From Space 和 `To Space。

对象的转移 Eden→Survivor→Old Space
Eden 区里存放的是新生的对象;From Space 和 To Space 中存放的是每次垃圾回收后存活下来的对象,所以每次垃圾回收后,Eden 区会被清空;存活下来的对象先是放到 From Space,当 From Space 满了之后移动到 To Space;当 To Space 满了之后移动到 Old Space。Survivor 的两个区是对称的,没先后关系,所以同一个区中可能同时存在从 Eden 复制过来的对象和从前一个 Survivor 复制过来的对象,而复制到 Old Space 区的只有从第一个 Survivor 复制过来的对象。而且,Survivor 区总有一个是空的。同时,根据程序需要,Survivor 可以配置多个(多于 2 个),这样可以增加对象在 Young Generation 中存在的时间,减少被放到 Old Generation 的可能。
Old Space 中则存放生命周期比较长的对象,而且有些比较大的新生对象也放在 Old Space 中。

小结

每当有线程被创建的时候,JVM 就需要为其在内存中分配虚拟机栈和本地方法栈来记录调用方法的内容,分配程序计数器记录指令执行的位置,这样的内存消耗就是创建线程的内存代价。

面试题

堆内存都是线程共享的吗?

堆内存并不是完完全全的线程共享,其 eden 区域中还是有⼀部分空间是分配给线程独享的。这⾥值得注意的是,我们说 TLAB 是线程独享的,但是只是在 " 分配 " 这个动作上是线程独享的,至于在读取、垃圾回收等动作上都是线程共享的,而且在使用上也没有什么区别。

TLAB 是虚拟机在堆内存的 eden 划分出来的⼀块专用空间。

开线程影响哪块内存?

JVM 启动时会分配和 HeapMethod Area 线程共享的内存区域;每当有线程被创建的时候,JVM 就需要为其在内存中分配虚拟机栈本地方法栈来记录被调用方法的内容,分配程序计数器记录指令执行的位置,这样的内存消耗就是创建线程的内存代价。

为什么会出现 StackOverflowError 异常?

每启动一个线程,JVM 都会为其分配一个 Java 虚拟机栈,线程私有的,每调用一个方法,都会被封装成一个栈帧,进行压栈操作,当方法执行完成之后,又会执行弹栈操作。而每个栈帧中,当前调用的方法的一些局部变量、动态连接,以及返回地址等数据。
elckn
每次方法的调用,执行压栈的操作,但是每个栈帧,都是要消耗内存的。一旦超过了限制,就会爆掉,抛出 StackOverflowError。
虚拟机栈默认大小:
io4e6